RustでSQLite3のextension(拡張ライブラリ)を作成する
Introduction
以前SQLiteのextensionを使う記事を書いたのですが、
今回はextensionを作るほうです。
sqlite-loadable-rsを使えばRustでSQLite用extensionを作成できるので、
やってみましょう。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 13.5.2
- Linux VM : ubuntu23(OrbStack使用)
- Rust : 1.72.1
Setup
sqlite-loadable-rsクレートはRustを使用してSQLite拡張を構築するためのライブラリです。
詳細についてはここに紹介記事がありますのでご確認ください。
sqlite-loadable-rsのリポジトリにはそのまま動くサンプルがあるので、
Githubからcloneして動かしてみましょう。
Linux VM環境を作成
私の環境(M1 mac)だと動かなかったので、Linux VM環境を用意します。
OrbStack使ったら簡単に動いたのでそれを使います。
OrbStackを起動したらMachinesからUbuntu23を選択して起動します。
起動したら↓のようにsshでログインできます。
とても楽です。
% ssh <作成したVMの名前>@orb
Ubuntuで必要なライブラリなどのインストールをします。
% sudo apt update % sudo apt-get install git % sudo apt-get install -y sqlite3 % sudo apt-get install -y gcc % sudo apt-get install libssl-dev
私の環境ではビルド時にssl関連のエラーがでたので、
そうなった場合は↓のコマンドでパスをしらべて環境変数に設定。
% dpkg -L libssl-dev | grep lib % dpkg -L libssl-dev | grep include #自分の環境では↓を設定 % export OPENSSL_INCLUDE_DIR=/usr/include/openssl/ % export OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu/
そしてRustとNodeのインストールをしておきます。
# Rust % curl https://sh.rustup.rs -sSf | sh % export PATH=$PATH:$HOME/.cargo/bin # nodeとnpm % sudo apt install nodejs npm
Try sqlite-loadable-rs
では次に、sqlite-loadable-rsにあるサンプルの拡張ライブラリを動かしてみましょう。
sqlite-loadable-rsをcloneしてhello extentionをうごかしてみます。
% git clone https://github.com/asg017/sqlite-loadable-rs.git % cd sqlite-loadable-rs
Rustで記述された、hello extentionをビルドします。
% cargo build --example hello --release
sqliteを起動して、↑でビルドしたモジュールをloadします。
hello関数が使えるようになってますね。
% sqlite3 SQLite version 3.40.1 2022-12-28 14:03:47 ・・・ sqlite> .load target/release/examples/libhello sqlite> select hello("world!"); hello, world! sqlite> .quit
Create SQlite Extension
次に、Sqlite Extensionを作ってみます。
fooという関数を定義し、その中ではhttpbin.orgにアクセスして
自身のIPをとってくるという(特に意味のない)処理をしてみます。
Cargoでプロジェクトを作成して依存ライブラリをaddします。
% cargo new sqlite-plugin && cd sqlite-plugin % cargo add sqlite-loadable % cargo add reqwest
Cargo.tomlは↓のような感じです。
[dependencies] reqwest = { version = "0.11.20", features = ["blocking", "json"] } sqlite-loadable = "0.0.5" [[example]] name = "foo" crate-type = ["cdylib"]
sqlite-plugin/examplesディレクトリを作成し、
foo.rsファイル(extension本体)を作成します。
use sqlite_loadable::prelude::*; use sqlite_loadable::{api, define_scalar_function, Result}; use std::collections::HashMap; pub fn foo(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> { let key = api::value_text(values.get(0).expect("1st argument as key"))?; let name = api::value_text(values.get(1).expect("2nd argument as name"))?; //外部サイトに接続してデータ取得 let resp = reqwest::blocking::get("https://httpbin.org/ip") .unwrap().json::<HashMap<String, String>>().unwrap(); api::result_text(context, format!("1st:{}, 2nd:{}, ip:{:?}", key,name,resp))?; Ok(()) } #[sqlite_entrypoint] pub fn sqlite3_foo_init(db: *mut sqlite3) -> Result<()> { let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC; define_scalar_function(db, "foo", 2, foo, flags)?; Ok(()) }
foo関数はそのまま"foo"という名前のscalar関数として登録されます。
この関数では引数を2つ受け取り、内部では自身のIPアドレスを外部サイトから取得して
引数とともに返します。
sqlite3_foo_init関数が外部に公開される拡張ライブラリ用のエントリーポイントです。
コンパイルされたファイルは"libfoo.so"(OSによっては.dylib/.dll)となります。
ではextensionをビルドしてみましょう。
% cargo build --example foo --release
ビルドに成功すると、target/release/examplesにlibfooファイルができてます。
% ls ./sqlite-plugin/target/release/examples/ foo-bf6dda611cc76dbe.d libfoo-bf6dda611cc76dbe.so libfoo.d libfoo.so
SQLiteのシェルから使う
sqlite3を起動してextensionをloadして使ってみます。
ちゃんと動いてますね。
% sqlite3 mydb.sqlite3 sqlite> .load target/release/examples/libfoo sqlite> select foo("hello","world"); 1st:hello, 2nd:world, ip:{"origin": "xxx.xxx.xxx.xxx"}
Nodeから使ってみる
プログラムからも使ってみます。
Node用SQlite3モジュールをインストールして、Nodeからアクセスしてみましょう。
% npm install sqlite3
↓のようにloadExtension関数を使って簡単に実装できます。
var sqlite3 = require('sqlite3'); //sqlite3データベースのパスを指定 let db = new sqlite3.Database("<your sqlite3 database file>", (err) => { if (err) { console.error(err.message) throw err }else{ console.log('Connected to the SQLite database.'); } }); //libfooへのパスを指定 db.loadExtension('<path/your/target/release/examples/libfoo>'); db.all('select foo("hello","world")', (error, rows) => { console.log(rows); }); db.close();
rustqliteから使ってみる
Rustからもextensionにアクセスしてみます。
rusqliteを使えばさきほどと同じように実行可能です。
適当なRustプロジェクトを作成し、Cargo.tomlにrussqliteを記述します。
※load_extensionをfeaturesで記述すること
[dependencies] rusqlite = { version = "0.29.0", features = ["bundled","load_extension"] }
Rustコードは↓のような感じです。
LoadExtensionGuardとload_extensionを使ってextensionをloadします。
unsafeを使ってるのはしょうがないところ。
use rusqlite::{Connection, Result,LoadExtensionGuard}; fn load_my_extension(conn: &Connection) -> Result<(),rusqlite::Error> { unsafe { let _guard = LoadExtensionGuard::new(conn)?; //libfooへのパスを指定 conn.load_extension("path/your/target/release/examples/libfoo", None) } } fn main() -> Result<(),rusqlite::Error>{ let conn = Connection::open_in_memory()?; if let Ok(_value) = load_my_extension(&conn) { let mut stmt = conn.prepare("select foo('hello','world')")?; let mut rows = stmt.query([])?; if let Some(row) = rows.next()? { let res:String = row.get(0)?; println!("{:?}",res); } } else { println!("load_my_extension Err"); } Ok(()) }
問題なく実行可能です。
% cargo run "1st:hello, 2nd:world, ip:{\"origin\": \"xxx.xxx.xxx.xxx\"}"
Summary
というわけで、RustでSqliteのextensionを実装して使ってみました。
今回はシンプルな関数を実装しましたが、virtual table用のモジュールも作成できます。
virtual tableを使えばいろいろなオブジェクト(ex.csv,s3,kvsなど)を
テーブルとしてアクセスできるので、さらに便利に使えそうです。